﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Text.RegularExpressions;
using System.IO;
using log4net;

using LanguageDetector;

namespace LanguageDetector.TrainerConsole
{
    class Program
    {
        #region vars & properties

        ILog logger = LogManager.GetLogger(typeof(Program));
        readonly static List<string> EXTENSIONS = new List<string> {
            "txt",
            "xml",
            "html",
            "htm",
            "rss",
        };

        Option[] _options = new Option[] 
        {
            new Option("id", OptionType.Language, @"^\s*([0-9]+)\s*$") { Comment = "Integer only language ID" },
            new Option("iso", OptionType.Language, @"^\s*([a-z]{2}(?:[-_][a-z]{2})?)\s*$") { Comment = "Alpha ISO code (2-5 characters)" },
            new Option("name", OptionType.Language, @"^\s*([^\s]+)\s*$") { Comment = "Common English name of the language" },
            
            new Option("ascii", OptionType.Optional, @"^\s*(true|false|0|1)\s*$") { Comment = "Includes ASCII character during training" },
            new Option("tmeout", OptionType.Optional, @"^\s*([0-9]+)\s*$") { Comment = "Timeout in seconds" },
            new Option("referer", OptionType.Optional, @"^([^$]+)$") { Comment = "Refering header data" },
            new Option("recursive", OptionType.Optional, @"^\s*(true|false|0|1)\s*$") { Comment = "Folder only option" },

            new Option("file", OptionType.Source, @"^([^$]+)$") { Comment = "UNC or local file path to training text file" },
            new Option("folder", OptionType.Source, @"^([^$]+)$") { Comment = "UNC or local path to folders containing txt or xml files only" },
            new Option("web", OptionType.Source, @"^((?:https?|ftps?)[^$]+)$") { Comment = "Fetch file over FTP or HTTP" },
            new Option("input", OptionType.Source, @"^([^$]+)$") { Comment = "Direct input string via console" },
        };

        List<Exception> _errors = new List<Exception>();
        public List<Exception> Errors
        {
            get { return _errors; }
        }

        LanguageStat _stat = null;
        public LanguageStat Language
        {
            get { return _stat; }
        }

        Source _source = null;
        public Source Source
        {
            get { return _source; }
        }

        ConsoleBuilder _console = new ConsoleBuilder();
        public string ConsoleOut
        {
            get { return _console.ToString(); }
        }
        #endregion

        #region ctor & helper methods

        public Program(string[] args)
        {
            try
            {
                var errors = new List<Exception>();
                if (args.IsNullOrEmpty())
                    _console.AppendLine(PrintHelp());
                else if (args.Length % 2 == 1)
                    _console.AppendLine("Missing arguments or parameters!!!");
                else
                    Init(args);
            }
            catch (Exception ex)
            {
                _errors.Add(ex);
                logger.Error("CTOR error", ex);
                _console.AppendLine("Error: {0}", ex);
            }
        }

        void Init(string[] args)
        {
            var aMap = new Dictionary<string, string>();
            for (var i = 0; i < args.Length; i += 2)
            {
                var k = args[i].Clean(CleanLevel.Lower | CleanLevel.Trim);
                if (!string.IsNullOrEmpty(k) && k.Length >= 2)
                {
                    k = k.Substring(1);
                    var v = args[i + 1];
                    if (!string.IsNullOrEmpty(v))
                    {
                        if (!aMap.ContainsKey(k))
                            aMap.Add(k, v);
                        else
                            aMap[k] = v;
                    }
                }
            }
            var fMap = (from p in aMap
                        join o in _options on p.Key equals o.Flag
                        let t = MergeOptionArgs(o, p.Value)
                        where t != null
                        select t).ToDictionary(i => i.Flag);
            if (fMap.IsNullOrEmpty())
                _console.AppendLine("Arguments format does not parse correctly.");
            else
                ExtractStatSource(fMap, out _stat, out _source);
        }

        void ExtractStatSource(Dictionary<string, Option> fMap, out LanguageStat stat, out Source src)
        {
            var langCount = (from i in fMap.Values where i.OptionType == OptionType.Language select 1).Sum();
            var sources = (from i in fMap.Values where i.OptionType == OptionType.Source select i).ToList();
            if (langCount >= 1 && sources.Count == 1)
            {
                stat = new LanguageStat();
                if (fMap.ContainsKey("id"))
                    stat.ID = int.Parse(fMap["id"].Value);
                if (fMap.ContainsKey("iso"))
                    stat.ISO2 = fMap["iso"].Value;
                if (fMap.ContainsKey("name"))
                    stat.Name = fMap["name"].Value;

                if (fMap.ContainsKey("ascii"))
                    stat.IncludeASCII = bool.Parse(fMap["ascii"].Value);

                src = new Source() { Path = sources[0].Value };
                if (fMap.ContainsKey("timeout"))
                    src.Tmeout = int.Parse(fMap["timeout"].Value);
                if (fMap.ContainsKey("referer"))
                    src.Referer = fMap["referer"].Value;
                if (fMap.ContainsKey("recursive"))
                    src.Recursive = bool.Parse(fMap["recursive"].Value);
                switch (sources[0].Flag)
                {
                    case "file":
                        src.Type = SourceType.File;
                        break;
                    case "folder":
                        src.Type = SourceType.Folder;
                        break;
                    case "web":
                        src.Type = SourceType.Web;
                        break;
                    default:
                        src.Type = SourceType.Input;
                        break;
                }
            }
            else
            {
                stat = null;
                src = null;
                _console.AppendLine("At least 1 LANGUAGE flag and only 1 SOURCE flag is required.");
            }
        }

        Option MergeOptionArgs(Option op, string arg)
        {
            if (op.RE.IsMatch(arg))
            {
                op.Value = arg;
                return op;
            }
            else
                return null;
        }

        public string PrintHelp()
        {
            var sb = new StringBuilder(this.GetType().Assembly.GetName().Name);
            sb.Append(" -<flag1> <value1> -<flag2> <value2> ...\r\n\r\n");
            sb.Append("At least 1 LANGUAGE flag and only 1 SOURCE flag is required.\r\n");
            sb.Append("When training a new language, all LANGUAGE & OPTIONAL flags should be filled.\r\n\r\n");
            var values = Enum.GetValues(typeof(OptionType));
            foreach (var v in values)
            {
                var t = (OptionType)v;
                sb.AppendFormat("{0} flag:\r\n", t.ToString().ToUpper());
                var ops = (from o in _options where o.OptionType == t select o).ToArray();
                for (var i = 0; i < ops.Length; i++)
                {
                    var o = ops[i];
                    sb.AppendFormat("\t-{0} <value> : {1}\r\n", o.Flag, o.Comment);
                }
            }
            return sb.ToString();
        }

        #endregion

        #region run core methods

        public void Run()
        {
            try
            {
                var started = DateTime.Now;
                if (_stat != null && _source != null)
                {
                    var logic = new TrainerLogic(_stat, o => o.ToString());
                    switch (_source.Type)
                    {
                        case SourceType.File:
                            logic.Run(File.ReadAllText(_source.Path));
                            break;
                        case SourceType.Folder:
                            logic.AutoSaveToDB = false;
                            var pf = new ProcessFolder(logic, _source.Path, _source.Recursive, _console);
                            pf.Run(); //multi-threaded...
                            logic.SaveResults();
                            break;
                        case SourceType.Web:
                            logic.Run(FetchWebData(_source.Path));
                            break;
                        case SourceType.Input:
                        default:
                            logic.Run(_source.Path);
                            break;
                    }
                    _errors.AddRange(logic.Errors);
                    _console.AppendLine("Training completed. Took: {0}. {1} total errors.", DateTime.Now.Subtract(started), _errors.Count);
                }
            }
            catch (Exception ex)
            {
                _errors.Add(ex);
                logger.Fatal("Fatal error executing program", ex);
                _console.AppendLine("Error: {0}", ex);
            }
        }

        string FetchWebData(string path)
        {
            string data = null;
            try
            {
                var wget = WebRequest.Create(path) as HttpWebRequest;
                var wres = wget.GetResponse(); //no boxing
                using (var reader = new StreamReader(wres.GetResponseStream()))
                {
                    data = reader.ReadToEnd();
                }
            }
            catch (Exception ex)
            {
                _errors.Add(ex);
                logger.Error("Error fetching web data.", ex);
                _console.AppendLine("FetchWebData Error: {0}", ex);
            }
            return data;
        }

        #endregion

        #region main method

        static void Main(string[] args)
        {
            var proc = new Program(args);
            proc.Run();
        }

        #endregion
    }
}
